package org.apache.ibatis.common.logging.jdbc;

import org.apache.ibatis.common.logging.Log;
import org.apache.ibatis.common.reflection.ArrayUtil;

import java.lang.reflect.Method;
import java.sql.Array;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.*;
import java.util.stream.Collectors;

/**
 * @Name BaseJdbcLogger
 * @Description: [JDBC 代理（日志增强）] 基类
 * 描述信息：MyBatis 是一个 ORM 框架，只负责数据库信息和 JAVA 对象的相互映射操作，而不负责具体的数据库读写操作。具体的数据库读写操作是由 JDBC 进行的。然而，JDBC 日志有自身的一套输出体系，与 MyBatis 日志是分开的，这会给我们的调试工作带来很多的困难。
 * 我们可以基于代理模式，对 JDBC 操作进行日志增强，让 MyBatis 能够将 JDBC 的操作日志打印出来，极大地方便了我们的调试工作。
 * 该类作为基类提供了一些子类会用到的一些基本功能，而其他几个实现子类使用动态代理来为相应的 JDBC 类增强日志打印能力。
 * @Version v1.0.0
 * @Author: lichao
 * @Date: 2024-05-15 16:31:14
 **/
public abstract class BaseJdbcLogger {
    // 记录执行 SQL 操作的方法名称，这些方法名称是固定的，因此是常量
    protected static final Set<String> EXECUTE_METHODS = new HashSet<>();
    // 记录 PreparedStatement 中所有 Setter 方法的名称
    protected static final Set<String> SET_METHODS;
    // 记录 PreparedStatement 中的所有 Setter 方法中的 key:value，即属性和值
    private final Map<Object, Object> columnMap = new HashMap<>();
    // 记录 PreparedStatement 中的所有 Setter 方法中的 key，即属性
    private final List<Object> columnNames = new ArrayList<>();
    // 记录 PreparedStatement 中的所有 Setter 方法中的 value，即值
    private final List<Object> columnValues = new ArrayList<>();
    // 选取的 Log 实现类
    protected final Log statementLog;
    // SQL 操作的嵌套层数
    protected final int queryStack;

    static {
        SET_METHODS = Arrays.stream(PreparedStatement.class.getDeclaredMethods())
                .filter(method -> method.getName().startsWith("set"))
                .filter(method -> method.getParameterCount() > 1)
                .map(Method::getName)
                .collect(Collectors.toSet());
        EXECUTE_METHODS.add("execute");
        EXECUTE_METHODS.add("executeUpdate");
        EXECUTE_METHODS.add("executeQuery");
        EXECUTE_METHODS.add("addBatch");
    }

    public BaseJdbcLogger(Log log, int queryStack) {
        this.statementLog = log;
        if (queryStack == 0) this.queryStack = 1;
        else this.queryStack = queryStack;
    }

    protected void setColumn(Object key, Object value) {
        columnMap.put(key, value);
        columnNames.add(key);
        columnValues.add(value);
    }

    protected Object getColumn(Object key) {
        return columnMap.get(key);
    }

    protected String getParameterValueString() {
        List<Object> typeList = new ArrayList<>(columnValues.size());
        for (Object columnValue : columnValues) {
            if (columnValue == null) typeList.add("null");
            else typeList.add(objectValueString(columnValue) + "(" + columnValue.getClass().getSimpleName() + ")");
        }
        final String parameters = typeList.toString();
        return parameters.substring(1, parameters.length() - 1);
    }

    protected String objectValueString(Object columnValue) {
        if (columnValue instanceof Array) {
            try {
                return ArrayUtil.toString(((Array) columnValue).getArray());
            } catch (SQLException e) {
                return columnValue.toString();
            }
        }
        return columnValue.toString();
    }

    protected String getColumnString() {
        return columnNames.toString();
    }

    protected void clearColumnInfo() {
        columnMap.clear();
        columnNames.clear();
        columnValues.clear();
    }

    protected String removeBreakingWhitespace(String original) {
        StringTokenizer whitespaceStripper = new StringTokenizer(original);
        StringBuffer sb = new StringBuffer();
        while (whitespaceStripper.hasMoreTokens()) {
            sb.append(whitespaceStripper.nextToken()).append(" ");
        }
        return sb.toString();
    }

    protected boolean isDebugEnabled() {
        return statementLog.isDebugEnabled();
    }

    protected boolean isTraceEnabled() {
        return statementLog.isTraceEnbaled();
    }

    protected void debug(String text, boolean input) {
        if (statementLog.isDebugEnabled()) {
            statementLog.debug(prefix(input) + text);
        }
    }

    protected void trace(String text, boolean input) {
        if (statementLog.isTraceEnbaled()) {
            statementLog.trace(prefix(input) + text);
        }
    }

    private String prefix(boolean isInput) {
        char[] buffer = new char[queryStack * 2 + 2];
        Arrays.fill(buffer, '=');
        buffer[queryStack * 2 + 1] = ' ';
        if (isInput) buffer[queryStack * 2] = '>';
        else buffer[0] = '<';
        return new String(buffer);
    }

}

